/**
 * @file URL处理方法集合，keUrl类定义。
 * @module ke-url/url
 * @exports ke-url/url
 * @requires lodash
 * @requires node/url
 * @requires ke-url/path
 * @requires ke-url/query
 * @requires ke-url/options
 */

import * as _ from 'lodash';
import * as nodeUrl from 'url';
import {isAbsolute, normalize, resolve} from './path';
import {QueryObject, buildQuery, parseQuery, encode, decode} from './query';
import {DebugLevel, setDebug, isDebug, debug, setDebugHandle} from './options';

const PROTOCOL_NOISE = ':/ ';
const HOST_NOISE = ':./ ';
const PATH_NOISE = '. ';
const QUERY_NOISE = '? ';
const HASH_NOISE = '# ';
const MAX_PORT = 65535;

// 路径末尾的文件匹配正则表达式
const reTrailingFile = /(\/?[^\/]+\.[^\/]+)$/i;
const reSchemeNoise = /[^a-z0-9\+\.\-]/ig; // REC 3986 @link https://tools.ietf.org/html/rfc3986#section-3.1
const reHostnameNoise = /[^a-z0-9-_.]/ig;

// 过滤器
const filters = {

	protocol(value): string {
		let result = _.trim(_.toString(value), PROTOCOL_NOISE).replace(reSchemeNoise, '');
		return _.toLower(result);
	},

	auth(value): string {
		let result = _.trim(_.toString(value), PROTOCOL_NOISE);
		return result;
	},

	username(value): string {
		return decode(_.toString(value));
	},

	password(value): string {
		return decode(_.toString(value));
	},

	hostname(value): string {
		let result = _.trim(_.toString(value), HOST_NOISE).replace(reHostnameNoise, '');
		return result.toLowerCase();
	},

	port(value): string {
		value = _.toString(value);
		let iValue = _.toInteger(value);
		return (iValue && iValue <= MAX_PORT) ? value : '';
	},

	path(value): string {
		value = _.toString(value);
		let path = _.trim(value, PATH_NOISE);
		if (path) {
			path = normalize(decode(path));
		}
		return path;
	},

	query(value): QueryObject {
		if (_.isString(value)) {
			value = _.trimStart(value, QUERY_NOISE);
			return value ? parseQuery(value) : {};
		}
		return value;
	},

	hash(value): string {
		let result = _.trimStart(_.toString(value), HASH_NOISE);
		return result;
	}
};

// 这里输出，主要为了方便typescript引用使用
export interface UrlObject {

	// URL协议
	protocol?: string;

	// URL验证信息
	auth?: string;
	username?: string;
	password?: string;

	// URL主机名、端口
	host?: string;
	hostname?: string;
	port?: string;

	// 路径
	path?: string;
	query?: string | QueryObject;
	hash?: string;
}

/**
 * URL数据的结构
 *
 * @interface UrlObject
 * @memberOf module:ke-url/url
 * @property {string} protocol            - URL的协议，字符串格式，且强制小写，过滤结尾的 `:` ，REC 3986，允许 `a-z0-9+-.` 字符构成）
 * @property {string} auth                - 用户信息，字符串格式，包含了 `username` 和 `password`
 * @property {string} username            - URL中的用户名，字符串格式
 * @property {string} password            - URL中的用户名，字符串格式
 * @property {string} host                - 字符串格式，包含 `hostname` 和 `port`
 * @property {string} hostname            - 主机名，字符串格式
 * @property {string} port                - 端口号，字符串格式的数字，保持和nodeUrl.parse的结果一致。
 * @property {string} path                - 路径名，字符串格式，与nodeUrl.parse不同（pathname），应该是一个绝对路径
 * @property {string|QueryObject} query   - 请求字符串，允许字符串和对象结构，不应该包含开头的 `?`
 * @property {string} hash                - 请求字符串，允许字符串，不应该包含开头的 `#`
 */


enum props {
	protocol = 0,
	username,
	password,
	hostname,
	port,
	path,
	query,
	hash,
}

let propsAry = [];
_.forEach(_.keys(props), prop => {
	if (typeof props[prop] === 'string') {
		propsAry[prop] = props[prop];
	}
});

let knownPorts = {
	http: 80,
	https: 433,
	ftp: 21,
	rtsp: 554,
	mms: 1755,
	file: 0,
};

/**
 * 注册或修改的URL协议的默认端口
 *
 * ```
 * registerKnownPort('http', 80);
 * registerKnownPort({
 *     http: 80,
 *     https: 443
 * });
 * ```
 *
 * @method
 * @memberOf module:ke-url/url
 * @param {string|Object<string, number>} protocol
 * @param {string|number|null} port
 * @returns {boolean}
 */
export function registerKnownPort(protocol: any, port?: number | string): boolean {
	if (_.isString(protocol) && typeof port !== 'undefined') {
		protocol = filters.protocol(protocol);
		let intPort = _.toInteger(port);
		if (protocol && intPort >= 0 && intPort <= MAX_PORT) {
			knownPorts[protocol] = intPort;
			return true;
		}
	}
	else if (_.isObject(protocol)) {
		let total = 0;
		_.forEach(_.keys(protocol), (_protocol) => {
			let _port = protocol[_protocol];
			if (registerKnownPort(_protocol, _port))
				total += 1;
		});
		return total > 0;
	}
	return false;
}

/**
 * 解析URL的用户信息
 *
 * @method
 * @memberOf module:ke-url/url
 * @param {string} auth 传入的用户信息
 * @param {any} result 指定一个结果对象，来装载解析出来的用户名（username）和密码（password）
 * @returns {Object} 返回包含用户信息的对象
 */
export function parseUserInfo(auth: string, result?: UrlObject): UrlObject {
	if (!result)
		result = <UrlObject>{};
	auth = filters.auth(auth);
	if (auth) {
		let username: string, password: string;
		[username, password] = auth.split(':');
		if (username) {
			result.username = decode(username);
			if (password)
				result.password = decode(password);
		}
	}
	return result;
}

/**
 * 解析主机名（端口）
 *
 * @method
 * @memberOf module:ke-url/url
 * @param {string} host 传入的主机名
 * @param {any} result 指定一个结果对象，来装载解析出来的主机名（hostname）和端口（port）
 * @returns {UrlObject}
 */
export function parseHost(host: string, result?: UrlObject): UrlObject {
	if (!result)
		result = <UrlObject>{};
	host = _.trim(host, HOST_NOISE);
	if (host) {
		let hostname: string, port: string;
		[hostname, port] = host.split(':');
		if (hostname) {
			result.hostname = hostname;
			var intPort = _.toInteger(port);
			if (intPort <= MAX_PORT)
				result.port = port;
		}
	}
	return result;
}

/**
 * 解析URL，并进行必要的解码和过滤。
 *
 * @method
 * @memberOf module:ke-url/url
 * @param {string|UrlObject|keUrl|Object} url 要解析的URL，允许字符串格式，或者对象结构。
 * @returns {UrlObject} 返回解析完的URL数据对象。
 * @property {string} protocol URL的协议名称
 */
export function parseUrl(url: any): UrlObject {
	let result = <UrlObject>{};
	let values = Array(propsAry.length),
		auth: string = '', host: string = '';
	let isRemoveProtocol = false;

	if (!url)
		return result;

	debug(DebugLevel.Trace, 'parseUrl', url);

	if (_.isString(url)) {
		({
			protocol: values[props.protocol],
			auth: auth, host: host,
			pathname: values[props.path],
			query: values[props.query], hash: values[props.hash],
		} = nodeUrl.parse(url, false, true));
	}
	else if (_.isObjectLike(url)) {
		if (url instanceof keUrl)
			return url.getData();
		({
			protocol: values[props.protocol],
			auth: auth, username: values[props.username], password: values[props.password],
			host: host, hostname: values[props.hostname], port: values[props.port],
			path: values[props.path],
			query: values[props.query], hash: values[props.hash],
		} = url);
		if (url.pathname)
			values[props.path] = url.pathname;
	}

	debug(DebugLevel.Trace, 'parseUrl matches', values);

	// auth
	if (auth) {
		({username: values[props.username], password: values[props.password]} = parseUserInfo(auth));
	}

	// host
	if (host) {
		({hostname: values[props.hostname], port: values[props.port]} = parseHost(host));
	}

	values.forEach((value, index) => {
		let prop = props[index];
		if (prop && typeof value !== 'undefined') {
			value = filters[prop] ? filters[prop](value) : value;
			result[prop] = value;
		}
	});

	if (isRemoveProtocol && result.protocol)
		delete(result.protocol);

	return result;
}

/**
 * 匹配路径的末尾是否为一个文件名
 *
 * ```
 * /.html => 并不是一个有效的文件末尾格式，将视作为一个目录 /.html/
 * /index.html/ 也视作路径 => 如果指定了末尾的目录分隔符，也将视作为一个目录。
 * ```
 *
 * @method
 * @memberOf module:ke-url/url
 * @param path {string}
 * @returns {RegExpMatchArray}
 */
export function matchTrailingFile(path: string) {
	return _.toString(path).match(reTrailingFile);
}

/**
 * keUrl类
 *
 * keUrl类的属性，允许外部直接通过属性进行修改。但是要获取有效安全的URL属性，应该尽量使用URL的方法，而不要直接访问属性。
 *
 * keUrl的属性，和UrlObject的属性相似。但去掉了 `auth`、`host` 两个重复的属性，保留最基础的数据属性。
 *
 * ```
 * var url = new keUrl('http://jack:12345@127.0.0.1:9091/');
 * url.username; // jack
 * url.password; // 12345
 * url.getUserInfo(); // jack:12345
 * url.hostname; // 127.0.0.1
 * url.port; // 9091
 * url.getHost() // 127.0.0.1:9091
 * url.getAuthority() // jack:12345@127.0.0.1:9091
 * ```
 *
 * @class keUrl
 * @memberOf module:ke-url/url
 * @property {string} protocol   - URL的协议，字符串格式，且强制小写，过滤结尾的 `:` ，REC 3986，允许 `a-z0-9+-.` 字符构成）
 * @property {string} username   - URL中的用户名，字符串格式
 * @property {string} password   - URL中的用户名，字符串格式
 * @property {string} hostname   - 主机名，字符串格式
 * @property {string} port       - 端口号，字符串格式的数字，保持和nodeUrl.parse的结果一致。
 * @property {string} path       - 路径名，字符串格式，与nodeUrl.parse不同（pathname），应该是一个绝对路径
 * @property {QueryObject} query - 请求字符串，允许字符串和对象结构，不应该包含开头的 `?`
 * @property {string} hash       - 请求字符串，允许字符串，不应该包含开头的 `#`
 *
 * @param {string} url 输入的URL
 * @param {Object} options @see module:ke-url/url.keUrl.setOptions
 */
export class keUrl {

	// 以下属性和方法不应该随意的在外部被修改
	/**
	 * 包含Authority
	 * @type {boolean}
	 * @default true
	 * @constant
	 */
	static WithAuthority = true;
	/**
	 * 不包含Authority
	 * @type {boolean}
	 * @default false
	 * @constant
	 */
	static WithoutAuthority = false;

	/**
	 * 不调试
	 * @type {DebugLevel}
	 * @default 0
	 */
	static DebugNone: number = DebugLevel.None;
	/**
	 * 普通调试等级
	 * @type {DebugLevel}
	 * @default 1
	 */
	static DebugLog: number = DebugLevel.Log;
	/**
	 * 跟踪调试等级
	 * @type {DebugLevel}
	 * @default 2
	 */
	static DebugTrace: number = DebugLevel.Trace;
	/**
	 * 错误调试等级（包括异常）
	 * @type {DebugLevel}
	 * @default 3
	 */
	static DebugError: number = DebugLevel.Error;

	/**
	 * @see {@link module:ke-url/options.setDebug}
	 */
	static setDebug = setDebug;
	/**
	 * @see {@link module:ke-url/options.isDebug}
	 */
	static isDebug = isDebug;
	/**
	 * @see {@link module:ke-url/options.setDebugHandle}
	 */
	static setDebugHandle = setDebugHandle;
	/**
	 * @see {@link module:ke-url/options.debug}
	 */
	static debug = debug;

	/**
	 * @see {@link module:ke-url/query.encode}
	 */
	static encode = encode;
	/**
	 * @see {@link module:ke-url/query.decode}
	 */
	static decode = decode;

	/**
	 * @see {@link module:ke-url/query.parseQuery}
	 */
	static parseQuery = parseQuery;
	/**
	 * @see {@link module:ke-url/query.buildQuery}
	 */
	static buildQuery = buildQuery;

	/**
	 * @see {@link module:ke-url/url.parseUserInfo}
	 */
	static parseUserInfo = parseUserInfo;
	/**
	 * @see {@link module:ke-url/url.parseHost}
	 */
	static parseHost = parseHost;
	/**
	 * @see {@link module:ke-url/url.parseUrl}
	 */
	static parseUrl = parseUrl;

	/**
	 * @see {@link module:ke-url/path.isAbsolute}
	 */
	static isAbsolute = isAbsolute;
	/**
	 * @see {@link module:ke-url/path.normalize}
	 */
	static normalize = normalize;
	/**
	 * @see {@link module:ke-url/path.resolve}
	 */
	static resolve = resolve;
	/**
	 * @see {@link module:ke-url/url.matchTrailingFile}
	 */
	static matchTrailingFile = matchTrailingFile;

	/**
	 * @see {@link module:ke-url/url.registerKnownPort}
	 */
	static registerKnownPort = registerKnownPort;

	protected options = {
		debugLevel: DebugLevel.None,
		withAuthority: true,
		trailingSeparator: true,
	};

	protocol?: string;
	username?: string;
	password?: string;
	hostname?: string;
	port?: string;
	path: string = '';
	query: QueryObject = {};
	hash?: string;

	/**
	 * 构建函数
	 * @constructs
	 * @param {string} url 输入的URL
	 * @param {Object} options @see module:ke-url/url.keUrl.setOptions
	 */
	constructor(url?: string | UrlObject, options?: Object) {
		this.merge(url);
		if (options)
			this.setOptions(options);
	}

	/**
	 * 合并数据到当前URL的数据中
	 *
	 * @param merge {string|UrlObject} 要合并的数据，可以是字符串类型，也可以是任意的对象类型（结构参考UrlObject）
	 * @returns {keUrl}
	 */
	merge(merge: string | UrlObject) {
		let data;
		if (merge instanceof keUrl)
			data = merge.getData();
		else
			data = parseUrl(merge);
		_.forEach(propsAry, prop => {
			if (typeof data[prop] !== 'undefined') {
				let segment = data[prop];
				if (prop === 'path')
					this.setPath(segment);
				else if (prop === 'query')
					this.mergeQuery(this.query, segment);
				else
					this[prop] = segment;
			}
		});
		return this;
	}

	/**
	 * 设置URL的选项
	 *
	 * debugLevel: 调试的级别
	 * withAuthority: 生成的URL是否包含完整信息，即包含了 [username[:password]@]hostname[:port]，默认为true
	 * trailingSeparator: 路径的末尾是否要强制包含路径的分隔符号（只在路径结尾为非文件格式时有效），默认为true
	 *
	 * @param {Object} options URL的设置选项
	 * @param {boolean} options.withAuthority - 设置在输出字符串URL时，是否包含Authority
	 * @param {boolean} options.trailingSeparator - 设置URL在输出路径时候，是否包含末尾的目录分隔符（当路径非文件名末尾时）
	 * @param {number} options.debugLevel - 设置URL实例的调试输出级别
	 * @returns {keUrl}
	 */
	setOptions(options: Object) {
		_.merge(this.options, options);
		return this;
	}

	/**
	 * 修改当前URL的路径
	 *
	 * 这个方法不是简单的将URL的path进行替换，而是同时包含了多级目录的拼接，已经. ..的处理（处理方法使用node.js path.js类库）
	 *
	 * 并且在处理时，会区分基础路径的末尾是否一个有效的文件名格式，会有不同的处理方式
	 *
	 * 基础路径：/hello/world
	 * ../test => /hello/test
	 * /user/../what => /hello/world/what
	 *
	 * 基础路径：/hello/world.html （以/hello为开始的目录，world.html为文件名）
	 * ../test => /test
	 * /user/../what => /hello/what
	 *
	 * @param paths {string[]}
	 * @returns {keUrl}
	 */
	setPath(...paths) {
		// 为了安全起见，还是要对每个路径进行解码处理，并过滤掉空白的值
		paths = _.map(paths, p => decode(p));
		let prefix = this.path;
		let matched = matchTrailingFile(prefix);
		if (matched) {
			// 匹配路径末尾为文件格式：file.ext
			// /.html 视作 => /.html/
			// /index.html/ 也视作路径
			// 将匹配的文件删除掉
			prefix = prefix.substr(0, matched.index);
		}
		let path = resolve(prefix, ...paths);
		if (!isAbsolute(path))
			path = '/' + path;
		// 末尾不为目录分隔符号
		if (!!this.options.trailingSeparator && path.charCodeAt(path.length - 1) !== 47) {
			path += '/';
		}
		this.path = path;
		return this;
	}

	/**
	 * 取得当前的路径
	 *
	 * @param trailingSep {boolean} 是否补充末尾的目录分隔符，如果不指定，则根据URL的Options决定。
	 * @returns {string}
	 */
	getPath(trailingSep = !!this.options.trailingSeparator): string {
		let path = this.path;
		if (trailingSep && path.charCodeAt(path.length - 1) !== 47) {
			let matched = matchTrailingFile(path);
			if (!matched) {
				path += '/';
			}
		}
		return path;
	}

	/**
	 * 合并多个请求字符
	 *
	 * @param merges {Array<string|QueryObject>}
	 * @returns {keUrl}
	 */
	mergeQuery(...merges) {
		_.merge(this.query, ...merges.map(item => filters.query(item)));
		return this;
	}

	/**
	 * 替换当前URL的请求字符数据
	 *
	 * @param query
	 * @returns {keUrl}
	 */
	replaceQuery(query?: string | QueryObject) {
		if (!query)
			this.query = {};
		else
			this.query = filters.query(query);
		return this;
	}

	/**
	 * 指定keys设置请求字符数据的值。
	 *
	 * @param keys {string|Array<string|number>} keys为写入数据的路径，允许字符串格式，如a[b][c][0]，或一个数组格式：['a', 'b', 'c', 0]，具体请参考 https://lodash.com/docs/4.16.6#set
	 * @param value
	 * @returns {keUrl}
	 */
	setQuery(keys, value) {
		_.set(this.query, keys, value);
		return this;
	}

	/**
	 * 移除指定keys的请求字符数据。
	 *
	 * @param path {string|Array<string|number>} keys为写入数据的路径，允许字符串格式，如a[b][c][0]，或一个数组格式：['a', 'b', 'c', 0]，具体请参考 https://lodash.com/docs/4.16.6#set
	 * @returns {keUrl}
	 */
	removeQuery(path) {
		_.set(this.query, path, null);
		return this;
	}

	/**
	 * 以当前URL的请求字符串数据为基础，生成字符串格式，允许传入多段要合并的请求字符串数据，以生成一个新的请求字符串。
	 *
	 * 注意，如果生成的请求字符串不为空，则会自动补充开头的 ?
	 *
	 * @param merges
	 * @returns {string}
	 */
	getQueryString(...merges): string {
		let query: QueryObject = this.query;
		if (merges.length > 0) {
			query = _.merge({}, this.query, ...merges.map(item => filters.query(item)));
		}
		let string = buildQuery(query);
		if (string)
			string = '?' + string;
		return string;
	}

	/**
	 * 设置用户信息
	 *
	 * @param userInfo {string} 用户信息
	 * @returns {keUrl}
	 */
	setUserInfo(userInfo: string) {
		parseUserInfo(userInfo, this);
		return this;
	}

	/**
	 * 获取URL包含的用户信息，并返回字符串格式 [username[:password]] ，如果URL不包含用户信息，则返回空字符串。
	 *
	 * @param isEncode {boolean}
	 * @returns {string}
	 */
	getUserInfo(isEncode = true): string {
		let auth = '';
		isEncode = !!isEncode;
		if (this.username) {
			auth = isEncode ? encode(this.username) : this.username;
			if (this.password)
				auth += ':' + (isEncode ? encode(this.password) : this.password);
		}
		return auth;
	}

	/**
	 * 判断端口号（port）是否为协议（protocol）的默认端口号。
	 *
	 * @param protocol {string}
	 * @param port {string|number}
	 * @returns {boolean}
	 */
	isKnownPort(protocol: string, port: string | number): boolean {
		protocol = filters.protocol(protocol);
		return (knownPorts[protocol] && knownPorts[protocol] === _.toInteger(port));
	}

	/**
	 * 取得URL的主机名（如果包含端口号，且指定了protocol，则连同端口号一起输出）
	 *
	 * @returns {string}
	 */
	getHost(): string {
		let host = '';
		if (this.hostname)
			host = _.toLower(this.hostname);
		if (host) {
			let port = _.toInteger(this.port);
			// 必须有protocol, port
			if (this.protocol && this.port && (port > 0 && port <= MAX_PORT) && this.isKnownPort(this.protocol, this.port))
				host += ':' + this.port;
		}
		return host;
	}

	/**
	 * 设置URL的主机名（允许包含端口）
	 *
	 * @param host
	 * @returns {keUrl}
	 */
	setHost(host: string) {
		parseHost(host, this);
		return this;
	}

	/**
	 * 设置URL的options.withAuthority
	 *
	 * @param withAuthority
	 * @returns {keUrl}
	 */
	setWithAuthority(withAuthority) {
		this.options.withAuthority = !!withAuthority;
		return this;
	}

	/**
	 * 生成Authority的字符串格式，如果URL不包含相关的有效数据（username, password, hostname, port）的话，将返回空字符串。
	 *
	 * Authority 包含 [username[:password]@]hostname[:port]
	 *
	 * @returns {string}
	 */
	getAuthority(): string {
		let result: string = '';
		let host: string = this.getHost(), userInfo = this.getUserInfo(true);
		if (host) {
			result = (userInfo ? userInfo + '@' : '') + host;
		}
		return result;
	}

	/**
	 * 生成字符串的URL
	 *
	 * @param withAuthority 是否包含Authority信息
	 * @returns {string}
	 */
	toUrl(withAuthority = !!this.options.withAuthority): string {
		let params: any = {
			pathname: this.getPath(),
			search: this.getQueryString(),
			hash: this.hash,
		};
		if (withAuthority && this.hostname) {
			params.protocol = this.protocol;
			params.auth = this.getUserInfo(false);
			params.host = this.getHost();
		}
		return nodeUrl.format(params);
	}

	toString(): string {
		return this.toUrl();
	}

	/**
	 * 基于当前URL的数据复制出一个新的URL
	 *
	 * @param merge {UrlObject}
	 * @returns {keUrl}
	 */
	clone(merge?: UrlObject) {
		let newUrl = new keUrl(this);
		if (merge)
			newUrl.merge(merge);
		return newUrl;
	}

	/**
	 * 当前URL有效的数据转为对象结构输出（Object）。
	 * @returns {UrlObject}
	 */
	getData(): UrlObject {
		let data = {};
		_.forEach(propsAry, prop => {
			if (this[prop]) {
				data[prop] = this[prop];
			}
		});
		return <UrlObject>data;
	}
}
